iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
佛心分享-SideProject30

Mongory:打造跨語言、高效能的萬用查詢引擎系列 第 28

Day 27:Go bridge Benchmark shock:原因拆解與方向

  • 分享至 

  • xImage
  •  

在 Ruby 版的震盪之後,Go 版也迎來一次「下巴掉下來」的時刻:

  • 純 Go:≈ 2ms(十萬筆簡單條件)
  • cgo 版 Mongory:≈ 90ms(同條件、同資料)

數字殘酷,但結論清楚:瓶頸不在演算法,而在 Go ↔ C 邊界的呼叫成本。本篇把觀測方法、原因拆解、與改善方向攤開來說


量測設計(與程式碼對齊)

基準程式(節錄):

size := 100_000
loops := 5
records := genRecords(size)

expectedSimple := countSimpleQuery(records)
expectedComplex := countComplexQuery(records)

bench("Simple query (Plain Go)", loops, func() { _ = countSimpleQuery(records) })

matcherSimple, _ := mongory.NewCMatcher(map[string]any{
  "age": map[string]any{"$gte": 18},
}, nil)
bench("Simple query (Mongory Matcher)", loops, func() {
  cnt := 0
  for i := range records { ok, _ := matcherSimple.Match(records[i]); if ok { cnt++ } }
  if cnt != expectedSimple { panic("count mismatch") }
})

bench("Complex query (Plain Go)", loops, func() { _ = countComplexQuery(records) })

matcherComplex, _ := mongory.NewCMatcher(map[string]any{
  "$or": []any{ map[string]any{"age": map[string]any{"$gte": 18}}, map[string]any{"status": "active"} },
}, nil)
bench("Complex query (Mongory Matcher)", loops, func() {
  cnt := 0
  for i := range records { ok, _ := matcherComplex.Match(records[i]); if ok { cnt++ } }
  if cnt != expectedComplex { panic("count mismatch") }
})

量測原則:

  • 關閉 GC 干擾(暫時):debug.SetGCPercent(-1),迴圈前後讀取 runtime.MemStats 觀察趨勢
  • 重複測量與校驗一致性(expect vs actual)
  • 重用 matcher 對齊 Ruby 版經驗,避免重複建構成本干擾

為什麼純 Go 2ms、cgo 90ms?

  • 邊界呼叫成本:
    • 每筆資料皆需呼叫 C 一次以上,cgo 帶來固定開銷(狀態切換、棧調整、調度)
    • shallow 取值雖已惰性、O(1) 索取,但仍需在多次取值中跨界
  • 反射成本(次要):
    • Go 端 shallow 包裝取值會使用反射,雖然已經壓到最少,但在十萬筆規模下仍可見
  • 設計哲學差異:
    • Ruby 版的 bottleneck 在 Ruby 本身運算,Go 版的 bottleneck 反而在 cgo 邊界

結論:當條件與資料結構足夠簡單時,純 Go 直敲記憶體是壓倒性優勢,任何跨界呼叫都會被放大


驗證假說:拆解實驗

  • 實驗 A:批次化呼叫
    • 將多筆資料合併成一個 C 呼叫進行匹配,觀察時間是否近似線性下降
  • 實驗 B:極簡 matcher 與零取值
    • 使用常數條件(不取資料)觀測 cgo 基本呼叫成本
  • 實驗 C:移除 to_string 與 trace
    • 削去所有可觀測輔助,確認不影響主結論

這些實驗能將「反射成本」與「cgo 固定成本」切開,驗證主要瓶頸確實在跨界次數


心情與決策

筆者第一次看到 2ms vs 90ms 的差距時,確實失望,但更重要的是它給了清晰的方向。與其在邊界上糾結,不如承認「Go 有它的最短路」:把核心改寫成 native Go,才是穩健的長期選擇。C Core 仍然會是 Ruby 與其他語言橋接的關鍵,但在 Go,筆者會押注 native 路線


小結

這次的 shock 再次提醒:效能是系統性的。Ruby 版靠 shallow O(1) 與 pool reset 贏回來,Go 版的贏法,則是直接不使用 cgo 橋接 Mongory-core,應在保留既有 DSL/AST 的前提下,直接使用 native Golang 重寫。


上一篇
Day 26:Go GC × memory pool:生命週期管理
下一篇
Day 28:為什麼 Go 需要 native 重寫:cgo overhead 與編譯器演進
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言